package com.hexus.eve.entities;

import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import javax.persistence.Transient;

import org.apache.log4j.Logger;
import org.hibernate.SessionFactory;
import org.hibernate.annotations.Cascade;
import org.hibernate.annotations.CascadeType;
import org.hibernate.annotations.LazyCollection;
import org.hibernate.annotations.LazyCollectionOption;
import org.springframework.orm.hibernate3.HibernateTemplate;

import com.beimin.eveapi.account.accountstatus.AccountStatusParser;
import com.beimin.eveapi.account.accountstatus.AccountStatusResponse;
import com.beimin.eveapi.account.characters.ApiCharacter;
import com.beimin.eveapi.account.characters.CharactersParser;
import com.beimin.eveapi.account.characters.CharactersResponse;
import com.beimin.eveapi.core.ApiAuthorization;
import com.beimin.eveapi.core.ApiException;
import com.hexus.eve.application.Constants;
import com.hexus.eve.application.Constants.Statuses;
import com.hexus.eve.entities.security.CipherEncrypter;
import com.hexus.eve.entities.security.User;
import com.hexus.eve.utils.EmptyItems;

/**
 * Class describes an EVE account.
 * @author Mikhail_Berastau
 */
@Entity
@Table(name = "user_accounts")
public class Account {
	@Transient
	private Logger log = Logger.getLogger(this.getClass());
	
	//Availability status of account (result of checking the API Key/userID pair)   
	@Transient
	private Constants.Statuses availabilityStatus = Statuses.NOT_CHECKED;
	
	//Timestamp of the latest successful check (i.e. when result was NOT 'UNAVAILABLE')
	@Transient
	private Date lastCheckDate = new Date();
	
	//Timestamp of the latest successful synchronization
	@Transient
	private Date lastSynchronizationDate = new Date();

	//Mask which will be used for displaying API Key, e.g. 12XXXXX89
	private static final String MASK_SYMBOLS = "XXXXX";

	@Id
	@GeneratedValue
	private Integer id;

	//Stores encrypted(!) API Key for storing in database
	private String apiKey = EmptyItems.STRING;
	
	@ManyToOne
	@JoinColumn(name = "username", nullable = false)
	private User user;

	//Stores decrypted API Key, used for authentication on EVE API server
	@Transient
	private String decodedApiKey = EmptyItems.STRING;

	//Stores User ID for authentication on EVE API server
	private Integer userId = EmptyItems.INTEGER;
	

	@OneToMany
	@LazyCollection(LazyCollectionOption.FALSE)
	@JoinTable(name="account_characters", joinColumns={@JoinColumn(name="accountId")}, inverseJoinColumns={@JoinColumn(name="id")})
	@Cascade(CascadeType.ALL)
	private List<Character> characters = new ArrayList<Character>();	
	
	
	/**
	 * Local key which is used for encryption
	 * @return local key 
	 */
	private String getLocalKey() {
		//User name is used as a local key - it is unique and user-specific
		return user.getName();
	}

	/**
	 * Sets API Key which will be used for accessing EVE API server
	 */
	public void setApiKey(String key) {
		CipherEncrypter ce = new CipherEncrypter(getLocalKey());
		apiKey = ce.encrypt(key);
		decodedApiKey = key;
	}

	/**
	 * Returns not masked API Key
	 */
	public String getApiKey() {
		//Decryption is expensive, we need to make it only once
		if (decodedApiKey == EmptyItems.STRING) {
			CipherEncrypter ce = new CipherEncrypter(getLocalKey());
			decodedApiKey = ce.decrypt(apiKey);
		}

		return decodedApiKey;
	}
	
	/**
	 * Returns masked API Key - used for security reasons when API Key is need to be displayed at a page.
	 * Example of the result: 12XXXXX89
	 */	
	public String getMaskedApiKey() {
		String apiKey = getApiKey();
		Integer apiKeyLength = apiKey.length();
		String masked = apiKey.length() > 2 ? apiKey.substring(0, 3)
				+ MASK_SYMBOLS
				+ apiKey.substring(apiKeyLength - 3, apiKeyLength)
				: EmptyItems.STRING;
		return masked;
	}

	/**
	 * Checks availability status.
	 * Method checks if EVE API successfully authenticates account's API Key / User ID pair.
	 * Last synch result can be retrieved by calling <code>getAvailabilityStatus()</code> method. 
	 * @return Result of the check (current status) 
	 */
	public Statuses updateAvailabilityStatus() {
		Statuses s = Statuses.NOT_CHECKED;
		AccountStatusParser asp = AccountStatusParser.getInstance();
		try {
			ApiAuthorization auth = new ApiAuthorization(userId, -1, getApiKey());
			AccountStatusResponse asr = asp.getResponse(auth);
			if (asr != null) {
				if(asr.hasError()) {
					s = Statuses.UNAVAILABLE;
					if(log.isDebugEnabled()) {
						log.debug("Account status response error: " + asr.getError() + " User ID=" + getUserId() + ", API Key (masked)=" + getMaskedApiKey());
					}
				}
				else {
					s = Statuses.AVAILABLE;
				}
			}
		} catch (ApiException e) {
			//s = Statuses.UNAVAILABLE;
			if(log.isDebugEnabled()) {
				log.debug(e.getMessage());
				//e.printStackTrace();
			}
		}

		availabilityStatus = s;
		
		if(s != Statuses.NOT_CHECKED)
			lastCheckDate = new Date();
		
		if(log.isDebugEnabled()) {
			log.debug("Account " + getId() + "; availability check result: " + availabilityStatus);
		}
		
		return s;
	}

	
	/**
	 * Updates availability status and takes in all new characters from EVE 
	 */
	public void synchronizeWithEve(SessionFactory sessionFactory) {
		if(!isAvailabilityStatusUpToDate()) {
			updateAvailabilityStatus();
		}
		
		if(getAvailabilityStatus() == Statuses.AVAILABLE) {
			takeInNewCharacters(sessionFactory);
		}
	}

	
	/**
	 * Method adds all EVE API characters which are not presented in the account yet (new characters)
	 * @param sessionFactory - used for saving new characters   
	 */
	public void takeInNewCharacters(SessionFactory sessionFactory) {
		if(getAvailabilityStatus() == Statuses.AVAILABLE) {
			CharactersParser cp = CharactersParser.getInstance();
			try {			
				ApiAuthorization auth = new ApiAuthorization(getUserId(), -1, getApiKey());
				CharactersResponse cr =  cp.getResponse(auth);
				if(!cr.hasError()) {
					//Getting all account characters via EVE API, creating map from them
					List<ApiCharacter> eveCharacters = (List<ApiCharacter>) cr.getEveCharacters();					
					HashMap<Long, ApiCharacter> eveCharactersMap = new HashMap<Long, ApiCharacter>();
					for(ApiCharacter eveCharacter : eveCharacters){						
						eveCharactersMap.put(eveCharacter.getCharacterID(), eveCharacter);
					};
					
					//Removing all characters which are already in account from the map
					for(Character character : characters) {
						if(eveCharactersMap.containsKey(character.getCharacterID())) {
							eveCharactersMap.remove(character.getCharacterID());
						}
					}
					
					HibernateTemplate ht = new HibernateTemplate(sessionFactory);
					//Only new items are in the map now. Adding all of them to the account.
					for(ApiCharacter newEveCharacter : eveCharactersMap.values()) {
						Character character = Character.convertApiCharacterToCharacter(newEveCharacter, this);
						ht.save(character); //saving new characters to DB (making them persistent)
						getCharacters().add(character);
						if(log.isDebugEnabled()){
							log.debug("New character with ID=" + character.getCharacterID() + " has been added to the account with ID=" + getId() + " (API Key=" + getMaskedApiKey() + ")");
						}
					}					
				}
				else {
					if(log.isDebugEnabled())
						log.debug("API response error: " + cr.getError());
				}
			} catch (ApiException e) {
				if(log.isDebugEnabled())
					log.debug("API exception: " + e.getMessage());
				e.printStackTrace();
			}
		}

	}

	
	/**
	 * Returns characters as map with IDs as keys and characters as values.
	 * @return Map of account's characters
	 */
	public Map<Integer, Character> getCharactersMap() {
		Map<Integer, Character> result = new HashMap<Integer, Character>();
		for(Character c : getCharacters()) {
			result.put(c.getId(), c);
		}
		return result;
	}
	

	/**
	 * Allows to check if availability check status is up to date basing on the time passed since the last check.
	 * If status is outdated, it should be updated by calling the <code>updateAvailabilityStatus()</code> method
	 *   
	 * @return <code>true</code> - if availability status is up to date
	 */
	public Boolean isAvailabilityStatusUpToDate(){
		Integer syncInterval = Integer.parseInt(Constants.getInstance().getProperty(Constants.PropertyNames.ACCOUNT_AVAILABILITY_CHECK_INTERVAL));
		return (new Date()).getTime() - getLastCheckDate().getTime() > (syncInterval * 1000);
	}
	
	
	/**
	 * Allows to check if last synchronization is up to date basing on the time passed since then.
	 * If it's outdated, account need to be synchronized by calling the <code>synchronizeWithEve()</code> method
	 *   
	 * @return <code>true</code> - if availability status is up to date
	 */
	public Boolean isSynchronizationUpToDate(){
		Integer syncInterval = Integer.parseInt(Constants.getInstance().getProperty(Constants.PropertyNames.ACCOUNT_SYNCHRONIZATION_INTERVAL));
		return (new Date()).getTime() - getLastSynchronizationDate().getTime() > (syncInterval * 1000);
	}
	
	
	public void setId(Integer id) {
		this.id = id;
	}

	public Integer getId() {
		return id;
	}

	public User getUser() {
		return user;
	}

	public void setUser(User user) {
		this.user = user;
	}

	public Integer getUserId() {
		return userId;
	}

	public void setUserId(Integer userId) {
		this.userId = userId;
	}
	
	public Date getLastCheckDate() {
		return lastCheckDate;
	}

	public void setLastCheckDate(Date lastCheckDate) {
		this.lastCheckDate = lastCheckDate;
	}

	public Constants.Statuses getAvailabilityStatus() {
		return availabilityStatus;
	}

	public void setAvailabilityStatus(Constants.Statuses status) {
		this.availabilityStatus = status;
	}

	public List<Character> getCharacters() {
		return characters;
	}

	public void setCharacters(List<Character> characters) {
		this.characters = characters;
	}

	public Date getLastSynchronizationDate() {
		return lastSynchronizationDate;
	}

	public void setLastSynchronizationDate(Date lastSynchronizationDate) {
		this.lastSynchronizationDate = lastSynchronizationDate;
	}
	
}
